package com.only4play.jwt.impl;


import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import com.only4play.common.constants.ErrorCodeEnum;
import com.only4play.common.exception.ApplicationException;
import com.only4play.common.utils.DateUtils;
import com.only4play.jwt.JwtCodec;
import com.only4play.jwt.JwtConfig;
import com.only4play.jwt.JwtPayload;
import com.only4play.jwt.constant.JwtConstant;

import java.time.LocalDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author liyuncong
 * @version 1.0
 * @file JwtCodeImpl.java
 * @brief JwtCodeImpl
 * @details JwtCodeImpl
 * @date 2022-03-23
 * Edit History
 * ----------------------------------------------------------------------------
 * DATE                     NAME               DESCRIPTION
 * 2022-03-23               liyuncong        Created
 */
public class JwtCodecImpl<T> implements JwtCodec<T> {

    private final ObjectMapper mapper;

    public JwtCodecImpl(ObjectMapper objectMapper) {
        this.mapper = objectMapper;
    }

    @Override
    public String encode(JwtPayload<T> payload, JwtConfig config) {
        final LocalDateTime now = LocalDateTime.now();
        final Date issuerAt = DateUtils.asDate(now);
        final Date expireDate = DateUtils.asDate(now.plusSeconds(config.getExpireSeconds()));
        final Map<String, Object> header = new HashMap<>();
        header.put("alg", "HS256");
        header.put("typ", "JWT");
        try {
            final String loginUserId = payload.getLoginUserId();
            final String loginUserName = payload.getLoginUserName();
            final String properties = mapper.writeValueAsString(payload.getProperties());
            final Algorithm algorithm = Algorithm.HMAC256(config.getSecret());
            return JWT
                    .create()
                    .withHeader(header)
                    .withClaim(JwtConstant.CLAIM_LOGIN_USER_ID, loginUserId)
                    .withClaim(JwtConstant.CLAIM_LOGIN_USER_NAME, loginUserName)
                    .withClaim(JwtConstant.CLAIM_PROPERTIES, properties)
                    .withIssuer(config.getIssuer())
                    .withIssuedAt(issuerAt)
                    .withExpiresAt(expireDate)
                    .sign(algorithm);
        } catch (JsonProcessingException exception) {
            throw new ApplicationException(ErrorCodeEnum.SERVER_ERROR, exception);
        }
    }

    @Override
    public JwtPayload<T> decode(String token, JwtConfig config) {
        Algorithm algorithm = Algorithm.HMAC256(config.getSecret());
        JWTVerifier verifier = JWT.require(algorithm).build();
        DecodedJWT jwt;
        jwt = verifier.verify(token);
        JwtPayload<T> payload = new JwtPayload<>();
        try {
            String loginUserId = jwt.getClaim(JwtConstant.CLAIM_LOGIN_USER_ID).asString();
            String loginUserName = jwt.getClaim(JwtConstant.CLAIM_LOGIN_USER_NAME).asString();
            T properties = mapper.readValue(
                    jwt.getClaim(JwtConstant.CLAIM_PROPERTIES).asString(),
                    new TypeReference<>() {
                    });
            payload.setLoginUserId(loginUserId);
            payload.setLoginUserName(loginUserName);
            payload.setProperties(properties);
            if (config.getValidateWhenDecode()) {
                payload.validate(config);
            }
        } catch (Exception exception) {
            throw new ApplicationException(ErrorCodeEnum.SERVER_ERROR, exception);
        }
        return payload;
    }

    @Override
    public boolean verify(String input, JwtConfig config) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(config.getSecret());
            JWTVerifier verifier = JWT.require(algorithm).build();
            verifier.verify(input);
        } catch (Exception exception) {
            return false;
        }
        return true;
    }
}
